跳到主要内容

反射基础

当我们需要检查或修改 Java 虚拟机中正在运行的应用程序的运行时行为时通常会使用反射。反射提高了程序的扩展性,程序可以通过使用外部用户定义类的完全限定名(fully-qualified names)来创建扩展性对象的实例或者获取类的信息。

我们在业务代码中很少用到反射,但是在一些框架中如 Spring,MyBatis 等都大量的使用了反射。一个最常见的例子是,Spring 创建 Bean 用的就是反射机制。

类的反射

通过类的完全限定名(fully-qualified names),我们可以获取到这个类的一个类实例。

方法一:Class.forName()

这是最常用的一种方法。

Class<?> clazz = Class.forName("tech.devguide.reflection.classload.model.User");
java

方法二:ClassLoader.loadClass()

Class<?> clazz = ClassLoadTest.class.getClassLoader().loadClass("tech.devguide.reflection.classload.model.User");
java


Class.forName() vs ClassLoader.loadClass()

两者都能获加载类,并且取到类的信息,但是两者是有一些区别的。

  • Class.forName()
    • 使用类加载器加装 class,类记载器为调用者的类加载器,一般为应用类加载器:AppClassLoader
    • 初始化类,所有静态成员变量会被初始化,静态代码块会被执行。
  • ClassLoader.loadClass()
    • 只加载类,并不初始化类。

实例化

方法一:使用Class#newInstance()方法实例化

Class<?> clazz = Class.forName("tech.devguide.reflection.classload.model.User");
User user = (User) clazz.newInstance();
java

这个方法从JDK9开始已被标记为过时。以下为关于过时的说明

This method propagates any exception thrown by the nullary constructor, including a checked exception. Use of this method effectively bypasses the compile-time exception checking that would otherwise be performed by the compiler. The Constructor.newInstance method avoids this problem by wrapping any exception thrown by the constructor in a (checked) InvocationTargetException.

The call

clazz. newInstance()

can be replaced by
clazz. getDeclaredConstructor().newInstance()

方法二:使用构造函数实例化

先对 User 类做如下修改:

@Data
public class User {

private String name;

private int age;

public User() {
}

public User(String name) {
this.name = name;
}

private User(String name, int age) {
this.name = name;
this.age = age;
}

}
java

使用无参构造函数实例化

Class<?> clazz = Class.forName("tech.devguide.reflection.classload.model.User");
Constructor<User> constructor = (Constructor<User>) clazz.getConstructor();
user = constructor.newInstance();
java

使用有参构造函数实例化

Class<?> clazz = Class.forName("tech.devguide.reflection.classload.model.User");
Constructor<User> constructor = (Constructor<User>) clazz.getConstructor(String.class);
User user = constructor.newInstance("张三");
System.out.println(user.getName());
java
一个兼容性问题

如果使用了lombok在获取有参构造函数时可能会报错,错误信息如下:

Fatal error compiling: java.lang.NoSuchFieldError:
Class com.sun.tools.javac.tree.JCTree$JCImport does not have member field 'com.sun.tools.javac.tree.JCTree qualid'
java

这是因为 lombok 版本太低,与高版本 JDK 不兼容的问题导致,lombok 支持 JDK 21 的最低版本为 1.18.30,具体可见:

使用私有有参构造函数实例化

Class<?> clazz = Class.forName("tech.devguide.reflection.classload.model.User");
Constructor<User> constructor = (Constructor<User>) clazz.getDeclaredConstructor(String.class, int.class);
constructor.setAccessible(true);
User user = constructor.newInstance("张三", 12);
System.out.println(user.getName());
java

这里需要注意的是需要通过 constructor.setAccessible(true) 设置下访问权限,否则该构造函数是无法访问的。


在通过参数获取构造函数时,要注意基本类型和包装类型之间是不等价的,如上面的例子,如果将 int.class 改为 Integer.class 是无法获取到构造函数的。

Class<?> clazz = Class.forName("tech.devguide.reflection.classload.model.User");
Constructor<User> constructor = (Constructor<User>) clazz.getDeclaredConstructor(String.class, Integer.class);
java

错误信息如下

Exception in thread "main" java.lang.NoSuchMethodException: tech.devguide.reflection.classload.model.User.<init>(java.lang.String,java.lang.Integer)
at java.base/java.lang.Class.getConstructor0(Class.java:3689)
at java.base/java.lang.Class.getDeclaredConstructor(Class.java:2858)
at tech.devguide.reflection.classload.ClassLoadTest.main(ClassLoadTest.java:29)
text

Note

这里的 <init> 就是构造函数。具体在《JVM上篇:内存与垃圾回收篇--类加载子系统》章节由详细的讲解

getDeclaredConstructorsgetConstructors 的区别

两者的区别在于 getConstructors 仅获取访问权限为 public 的构造函数,而 getDeclaredConstructors 能够获取到所有构造函数。

这篇文章中 《Difference between Loading a class using ClassLoader and Class.forName》 有更详细的说明。

构造函数 (Constructor) 实例中的信息

通过 Constructor 实例,可以获取到构造函数的修饰符、参数数量、参数信息、注解等信息。

Class<?> clazz = Class.forName("tech.devguide.reflection.classload.model.User");
Constructor<User> constructor = (Constructor<User>) clazz.getDeclaredConstructor(String.class, Integer.class);

// 获取构造方法修饰符
int modifier = constructor.getModifiers();
System.out.println(Modifier.isPrivate(modifier));

// 获取参数数量
int parameterCount = constructor.getParameterCount();
System.out.println(parameterCount);

// 获取参数类型
Class<?>[] parameterTypes = constructor.getParameterTypes();
System.out.println(parameterTypes);

// 获取参数信息
Parameter[] parameters = constructor.getParameters();
System.out.println(parameters);

// 获取构造函数的注解
Annotation[] declaredAnnotations = constructor.getDeclaredAnnotations();
System.out.println(declaredAnnotations);
java

字段处理

通过 Field 处理字段

通过 Class#getDeclaredField 方法可以获取到字段(java.lang.reflect.Field)实例。并通过 Field 实例设置字段值和获取字段值。

Field field = clazz.getDeclaredField("name");
field.setAccessible(true);
field.set(user, "张三"); // 设置name字段的值
String name = (String) field.get(user); // 获取name字段的值
System.out.println(name);
java

getField()getDeclaredField() 两者的区别

  • getField()
    • 仅获取 public 字段。
    • 如果本类中没有要查找的字段,会递归向上查找父类中是否包含这个字段。
  • getDeclaredField()
    • 包含所有字段
    • 仅查找本类,不会向上查找父类。

这时会有一个问题,如果我们要查找的字段在父类中,而这个字段是 private 字段,那么通过这两个方法都无法获取到这个字段。这时就需要在找不到所查字段时,获取父类信息,然后在父类上查找。以下工具类提供了类似的功能

  • org.apache.commons.lang3.reflect.FieldUtils#getField(Class, String)
  • org.springframework.security.util.FieldUtils#getField(Class, String)
  • cn.hutool.core.util.ReflectUtil#getField(Class, String)

通过 gettersetter 方法处理字段

Method setName = clazz.getDeclaredMethod("setName", String.class);
if (!setName.isAccessible()) {
setName.setAccessible(true);
}
setName.invoke(user, "李四");

System.out.println(user.getName());
java

isAccessible() 方法可判断方法是否有访问权限,如果没有访问权限,需要通过 setAccessible 方法修改访问权限。

getMethod()getDeclaredMethod() 两者的区别

  • getMethod()
    • 仅获取 public 方法。
    • 如果本类中没有要查找的方法,会递归向上查找父类中是否包含这个方法。
  • getDeclaredMethod()
    • 查找本类所有方法,不会向上查找父类。

通过 PropertyDescriptor 处理

PropertyDescriptorjava.beans 包下的一个类,用于处理类的字段。可以通过 PropertyDescriptor 获取到字段的名称,字段的get方法和字段的set方法等。Spring 中的 BeanUtils 工具类就大量了用了 PropertyDescriptor 来处理字段。

public static void main(String[] args) throws IntrospectionException, InvocationTargetException, IllegalAccessException {

User user = new User();
PropertyDescriptor propertyDescriptor = getPropertyDescriptor(User.class, "name");
Method writeMethod = propertyDescriptor.getWriteMethod();
writeMethod.invoke(user, "张三");

Method readMethod = propertyDescriptor.getReadMethod();
System.out.println(readMethod.invoke(user));
}

private static PropertyDescriptor getPropertyDescriptor(Class<?> clazz, String name) throws IntrospectionException {
BeanInfo beanInfo = Introspector.getBeanInfo(clazz);
PropertyDescriptor[] propertyDescriptors = beanInfo.getPropertyDescriptors();
for (PropertyDescriptor propertyDescriptor : propertyDescriptors) {
if (propertyDescriptor.getName().equals(name)) {
return propertyDescriptor;
}
}
return null;
}
java
信息

通过 BeanInfo 获取的 PropertyDescriptor 是包含父类字段的。

方法调用

通过-getter-和-setter-方法处理字段 小节中已经介绍过方法的获取和执行,此处不再赘述。

参考资料